package city.spring.configure;

import city.spring.configure.security.CustomJwtAccessTokenConverter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.jdbc.DataSourceProperties;
import org.springframework.boot.jdbc.DataSourceBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;
import org.springframework.security.config.core.GrantedAuthorityDefaults;
import org.springframework.security.oauth2.provider.ClientDetailsService;
import org.springframework.security.oauth2.provider.OAuth2RequestFactory;
import org.springframework.security.oauth2.provider.code.AuthorizationCodeServices;
import org.springframework.security.oauth2.provider.code.InMemoryAuthorizationCodeServices;
import org.springframework.security.oauth2.provider.code.JdbcAuthorizationCodeServices;
import org.springframework.security.oauth2.provider.request.DefaultOAuth2RequestFactory;
import org.springframework.security.oauth2.provider.token.DefaultTokenServices;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.JdbcTokenStore;
import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;
import org.springframework.security.oauth2.provider.token.store.JwtTokenStore;
import org.springframework.security.rsa.crypto.KeyStoreKeyFactory;

import javax.annotation.PostConstruct;
import javax.sql.DataSource;
import java.security.KeyPair;

/**
 * 配置Bean
 *
 * @author HouKunLin
 * @date 2019/12/4 0004 0:02
 */
@Configuration
public class ApplicationBeanConfiguration {
    private final static Logger logger = LoggerFactory.getLogger(ApplicationBeanConfiguration.class);
    /**
     * 令牌存储器数据源，本实例把令牌存储在其他的数据库中，与应用本省的数据源不同
     */
    private final DataSource dataSource;
    /**
     * 客户端信息Service
     */
    private final ClientDetailsService clientDetailsService;
    /**
     * 标记是否使用 JDBC 方式存储 OAuth2 信息
     */
    @Value("${spring.datasource.oauth2-use-jdbc:true}")
    private boolean oauth2UseJdbc;

    public ApplicationBeanConfiguration(DataSourceProperties properties,
                                        @Value("${spring.datasource.token.url}") String url,
                                        ClientDetailsService clientDetailsService) {
        // 配置 OAuth2 所需要使用的数据源，这里采用 OAuth2 和应用的数据源分离，采用两个库
        if (oauth2UseJdbc) {
            this.dataSource = DataSourceBuilder.create(properties.getClassLoader())
                    .type(properties.getType())
                    .url(url)
                    .username(properties.determineUsername())
                    .password(properties.determinePassword())
                    .driverClassName(properties.determineDriverClassName())
                    .build();
        } else {
            this.dataSource = null;
        }
        this.clientDetailsService = clientDetailsService;
    }

    /**
     * Jwt资源令牌转换器
     *
     * @return accessTokenConverter
     */
    @Bean
    public JwtAccessTokenConverter accessTokenConverter() {
        // 使用一个自定义的令牌转换器，该转换器加入了自定义头部信息，适配了资源服务器使用 JWK 方式进行授权验证
        JwtAccessTokenConverter jwtAccessTokenConverter = new CustomJwtAccessTokenConverter();
        // 必须设置这个 KeyPair ，否则在 /oauth/token_key 接口无法返回 token_key 信息，
        // 因此会导致资源服务器在启用jwt类型验证Token时，无法访问 /oauth/token_key 接口
        // 此时 /oauth/token_key 接口会在 L58 org.springframework.security.oauth2.provider.endpoint.TokenKeyEndpoint.getKey 抛出异常
        jwtAccessTokenConverter.setKeyPair(keyPair());
        return jwtAccessTokenConverter;
    }

    /**
     * 令牌存储器。
     * 用户登录成功的时候需要颁发令牌，然后把颁发的令牌存储起来，以便进行令牌校验判断用户是否登录成功。
     *
     * @return 令牌存储对象
     */
    @Bean
    public TokenStore tokenStore() {
        // return new InMemoryTokenStore();
        // 这里采用了 JDBC 的方式存储令牌，也就是存入到数据库中
        if (oauth2UseJdbc && dataSource != null) {
            return new JdbcTokenStore(dataSource);
        }
        return new JwtTokenStore(accessTokenConverter());
    }

    /**
     * 授权码存储器。
     * OAuth2 有一种授权码登录模式，该模式需要提供一个授权码存储器。
     *
     * @return 授权码存储器
     */
    @Bean
    public AuthorizationCodeServices jdbcAuthorizationCodeServices() {
        // 使用 JDBC 方式存储授权码信息
        if (oauth2UseJdbc && dataSource != null) {
            return new JdbcAuthorizationCodeServices(dataSource);
        }
        return new InMemoryAuthorizationCodeServices();
    }

    /**
     * 资源令牌转换器需要提供一个 KeyPair 对象。
     * 特别是资源服务器使用 JWK 方式校验令牌的时候，需要验证密钥信息，如果不手动提供密钥信息，每次重新启动授权服务器的时候，这个密钥信息会更新，
     * 因此会导致重启之前办法的令牌无法使用。
     *
     * @return KeyPair
     */
    @Bean
    public KeyPair keyPair() {
        /*
        参考文档：https://www.baeldung.com/spring-security-oauth2-jws-jwk#5-creating-a-keystore-file
        创建密钥文件jks命令：
        keytool -genkeypair -alias oauth-jwt -keyalg RSA -keypass oauth-pass -keystore oauth-jwt.jks -storepass oauth-pass
        JKS 密钥库使用专用格式。建议使用 "keytool -importkeystore -srckeystore oauth-jwt.jks -destkeystore oauth-jwt.jks -deststoretype pkcs12" 迁移到行业标准格式 PKCS12。
         */
        ClassPathResource ksFile = new ClassPathResource("oauth-jwt.jks");
        KeyStoreKeyFactory ksFactory = new KeyStoreKeyFactory(ksFile, "oauth-pass".toCharArray());

        return ksFactory.getKeyPair("oauth-jwt");
    }

    /**
     * 手动创建这个对象，用于在默认的用户登录操作（/login，非/oauth/token）使其可以自定义登录功能。
     *
     * @return DefaultTokenServices
     */
    @Bean
    public DefaultTokenServices defaultTokenServices() {
        DefaultTokenServices tokenServices = new DefaultTokenServices();
        tokenServices.setTokenStore(tokenStore());
        tokenServices.setSupportRefreshToken(true);
        // 设置为 false 在使用刷新 Token 功能时，能够清理旧的Token信息，从而创建新的Token
        // 能够解决在 true 状态下刷新Token会产生多个 access_token 而导致帐号无法登录的情况
        tokenServices.setReuseRefreshToken(false);
        tokenServices.setClientDetailsService(clientDetailsService);
        tokenServices.setTokenEnhancer(accessTokenConverter());
        return tokenServices;
    }

    /**
     * 手动创建这个对象，用于在默认的用户登录操作（/login，非/oauth/token）创建TokenRequest和OAuth2Request对象。
     *
     * @return OAuth2RequestFactory
     */
    @Bean
    public OAuth2RequestFactory requestFactory() {
        return new DefaultOAuth2RequestFactory(clientDetailsService);
    }

    /**
     * 设置默认的角色权限前缀信息（默认：ROLE_）。
     * org.springframework.security.access.expression.SecurityExpressionRoot.defaultRolePrefix
     *
     * @return GrantedAuthorityDefaults
     */
    @Bean
    public GrantedAuthorityDefaults grantedAuthorityDefaults() {
        // 如果想要自定义默认的角色前缀信息，则修改这里
        return new GrantedAuthorityDefaults("ROLE_");
    }

    @PostConstruct
    public void postConstruct() {
        logger.debug("密钥对: {}", keyPair());
        logger.debug("访问令牌转换器: {}", accessTokenConverter());
        logger.debug("令牌存储: {}", tokenStore());
    }
}
